import { DataSource, EntityMetadata, EntityTarget, QueryBuilder, QueryRunner } from 'typeorm'
import { RelationMetadata } from 'typeorm/metadata/RelationMetadata.js'
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata.js'
import { DriverUtils } from 'typeorm/driver/DriverUtils.js'
import { v4 } from 'uuid'
import { pick, uniq } from 'lodash-es'
import { Find } from './Find'
import { isObject } from '@vue/shared'
import { BroadcasterResult } from 'typeorm/subscriber/BroadcasterResult.js'
import { getProp, isFK, toArr, validateWhere } from './utils'
import { Update, UpdateMany } from './Update'

export type CreateOptions = {
  select?: any
  includes?: any
  data: any
  orIgnore?: boolean
}

class _Create extends QueryBuilder<any> {
  #options
  
  constructor(target: EntityTarget<any> | EntityMetadata, opt: CreateOptions, connection: DataSource, queryRunner?: QueryRunner) {
    super(connection, queryRunner)

    this.expressionMap.setMainAlias(this.createFromAlias(target instanceof EntityMetadata ? target.name : target))

    this.#options = opt
  }

  getQuery(): string {
    throw new Error('Method not implemented.')
  }

  async #execute(queryRunner: QueryRunner) {
    const { metadata } = this.expressionMap.mainAlias!, { connection } = this, { driver } = connection
    const { data, orIgnore } = this.#options
    const tableName = this.getTableName(this.getMainTableName())
    const isSQLite = DriverUtils.isSQLiteFamily(driver)
    
    const insertCols = [] as [ColumnMetadata, string][]
    const postCbs = [] as ((row: any) => any)[]
    
    for (const col of metadata.columns) {
      const v = col.getEntityValue(data)
      // process uuid
      if (v === undefined && col.isGenerated && col.generationStrategy == 'uuid' && !driver.isUUIDGenerationSupported()) {
        insertCols.push([col, this.createParameter(v4())])
      }
      // process default
      else if (v === undefined && col.default != null && (isSQLite || driver.options.type === "sap" || connection.driver.options.type === "spanner")) {
        insertCols.push([col, driver.normalizeDefault(col)!])
      }
    }
    
    for (const k in data) {
      const col = metadata.findColumnWithPropertyPathStrict(k) || metadata.findColumnWithDatabaseName(k)
      const rel = metadata.findRelationWithPropertyPath(k)
      if (col) {
        const v = isFK(col) ? data[col.databaseName] : col.getEntityValue(data)
        if (v === undefined) continue
        insertCols.push([col, this.createParameter(v)])
      }
      else if (rel) {
        const { connect, create, connectOrCreate } = data[k] || {}
        if (rel.isOneToOneOwner || rel.isManyToOne) {
          if (connect) {
            const reldata = await this.#obtainReldata(rel, connect, queryRunner)
            if (!reldata) {
              if (rel.isOneToOne) throw new Error(`No '${rel.inverseEntityMetadata.name}' record (needed to inline connect on create for '${metadata.name}' record) was found for a nested connect on one-to-one relation '${rel.propertyName}'.`)
              else throw new Error(`No '${rel.inverseEntityMetadata.name}' record(s) (needed to inline the relation on '${metadata.name}' record(s)) was found for a nested connect on one-to-many relation '${rel.propertyName}'.`)
            }
            if (rel.isOneToOneOwner) await this.#nullFK(rel, reldata, connection, queryRunner)
            rel.joinColumns.forEach(col => insertCols.push([col, this.createParameter(col.referencedColumn!.getEntityValue(reldata))]))
          }
          else if (create) {
            const reldata = await Create(rel.inverseEntityMetadata, { data: create }, connection, queryRunner)
            rel.joinColumns.forEach(col => insertCols.push([col, this.createParameter(col.referencedColumn!.getEntityValue(reldata))]))
          }
          else if (connectOrCreate) {
            const { create, where } = connectOrCreate
            let reldata = await this.#obtainReldata(rel, where, queryRunner)
            if (reldata && rel.isOneToOne) {
              await this.#nullFK(rel, reldata, connection, queryRunner)
            }
            else if (!reldata) {
              reldata = await Create(rel.inverseEntityMetadata, { data: create }, connection, queryRunner)
            }
            rel.joinColumns.forEach(col => insertCols.push([col, this.createParameter(col.referencedColumn!.getEntityValue(reldata))]))
          }
        }
        else if (rel.isOneToMany || rel.isOneToOneNotOwner) {
          if (connect) {
            if (rel.isOneToOne) {
              const reldata = await this.#obtainReldata(rel, connect, queryRunner)
              if (!reldata) throw new Error(`No '${rel.inverseEntityMetadata.name}' record to connect was found for a nested connect on one-to-one relation '${rel.propertyName}'.`)
              postCbs.push(async row => {
                const where = pick(reldata, rel.inverseEntityMetadata!.primaryColumns.map(e => e.propertyName))
                await this.#connect(rel, row, where, connection, queryRunner)
              })
            }
            else {
              postCbs.push(async row => {
                // todo Expected 1 records to be connected after connect operation on one-to-many relation 'author', found 0.
                for (const e of toArr(connect)) await this.#connect(rel, row, e, connection, queryRunner)
              })
            }
          }
          if (create) {
            postCbs.push(async row => {
              for (const e of toArr(create)) await this.#create(rel, row, e, connection, queryRunner)
            })
          }
          if (connectOrCreate) {
            for (const e of toArr(connectOrCreate)) {
              postCbs.push(async row => {
                const { create, where } = e
                let reldata = await this.#obtainReldata(rel, where, queryRunner)
                if (!reldata) await this.#create(rel, row, create, connection, queryRunner)
                else await this.#connect(rel, row, where, connection, queryRunner)
              })
            }
          }
        }
        else if (rel.isManyToMany) {
          const cols1 = rel.isOwning ? rel.junctionEntityMetadata!.ownerColumns : rel.junctionEntityMetadata!.inverseColumns
          const cols2 = rel.isOwning ? rel.junctionEntityMetadata!.inverseColumns : rel.junctionEntityMetadata!.ownerColumns
          if (connect) {
            for (const e of toArr(connect)) {
              postCbs.push(async row => {
                const reldata = await this.#obtainReldata(rel, e, queryRunner)
                if (!reldata) throw new Error(`Expected 1 records to be connected, found only 0.`)
                this.#createMid(rel, cols1, cols2, row, reldata, connection, queryRunner)
              })
            }
          }
          if (create) {
            for (const e of toArr(create)) {
              postCbs.push(async row => {
                const reldata = await Create(rel.inverseEntityMetadata, { data: e }, connection, queryRunner)
                this.#createMid(rel, cols1, cols2, row, reldata, connection, queryRunner)
              })
            }
          }
          if (connectOrCreate) {
            for (const e of toArr(connectOrCreate)) {
              postCbs.push(async row => {
                const { create, where } = e
                validateWhere(rel.inverseEntityMetadata, where, 'connectOrCreate.where')
                let reldata = await Find(rel.inverseEntityMetadata, { where, select: cols2.reduce((o, e) => (o[e.referencedColumn!.propertyName] = true, o), {} as any) }, connection, queryRunner)
                reldata ||= await Create(rel.inverseEntityMetadata, { data: create }, connection, queryRunner)
                this.#createMid(rel, cols1, cols2, row, reldata, connection, queryRunner)
              })
            }
          }
        }
      }
      else {
        throw new Error(`Unknown argument '${k}'.`)
      }
    }

    let exp =
      this.createComment() +
      this.createCteExpression() +
      isSQLite ? `insert` : 'INSERT'

    // process or ignore
    if (orIgnore) {
      if (DriverUtils.isSQLiteFamily(driver)) exp += ' OR IGNORE'
      else if (DriverUtils.isMySQLFamily(driver)) exp += ' IGNORE'
    }
    
    exp += ` INTO ${tableName}`

    // process columns values
    exp += ` (${insertCols.map(e => this.escape(e[0].databaseName)).join(',')})`
    exp += ` VALUES (${insertCols.map(e => e[1]).join(',')})`

    // process or ignore
    if (orIgnore && driver.supportedUpsertTypes.includes("on-conflict-do-update")) exp += ' ON CONFLICT DO NOTHING'

    // process returning
    const returnCols = uniq([...metadata.primaryColumns, ...insertCols.map(e => e[0])])
    exp += ` RETURNING ${returnCols.map(e => `${this.escape(e.databaseName)} AS ${this.escape(getProp(e))}`).join(',')}`
    // exp += ` RETURNING *`
    
    exp = this.replacePropertyNamesForTheWholeQuery(exp.trim())
    
    // run
    const [sql, params] = driver.escapeQueryWithParameters(exp, this.getParameters(), {})
    const [queryResult] = await queryRunner.query(sql, params)

    // call after insertion methods in listeners and subscribers
    if (this.expressionMap.callListeners === true && this.expressionMap.mainAlias!.hasMetadata) {
      const broadcastResult = new BroadcasterResult()
      // todo
      // queryRunner.broadcaster.broadcastAfterInsertEvent(broadcastResult, this.expressionMap.mainAlias!.metadata, valueSet)
    }
    
    // process postCbs
    for (const cb of postCbs) await cb(queryResult)

    return queryResult
  }

  async execute() {
    const { metadata } = this.expressionMap.mainAlias!
    const queryRunner = this.obtainQueryRunner()
    const { data } = this.#options

    const useTransaction = queryRunner.isTransactionActive ? false : Object.keys(data).some(k => metadata.findRelationWithPropertyPath(k))

    try {
      if (useTransaction) await queryRunner.startTransaction()

      const result = await this.#execute(queryRunner)

      if (useTransaction) await queryRunner.commitTransaction()

      return result
    }
    catch (e) {
      if (useTransaction) try { await queryRunner.rollbackTransaction() } catch (e) {}
      throw e
    }
    finally {
      if (queryRunner !== this.queryRunner) {
        await queryRunner.release()
      }
    }
  }

  #createMid(rel: RelationMetadata, cols1: ColumnMetadata[], cols2: ColumnMetadata[], row1: any, row2: any, connection: DataSource, queryRunner: QueryRunner) {
    const midData = {} as any
    cols1.forEach(e => midData[e.propertyName] = row1[e.referencedColumn!.propertyName])
    cols2.forEach(e => midData[e.propertyName] = row2[e.referencedColumn!.propertyName])
    return Create(rel.junctionEntityMetadata!.name, { data: midData, orIgnore: true }, connection, queryRunner)
  }

  async #obtainReldata(rel: RelationMetadata, where: any, queryRunner: any) {
    validateWhere(rel.inverseEntityMetadata, where, '')
    return await Find(rel.inverseEntityMetadata, {
      select: Object.fromEntries(rel.isOwning ? rel.joinColumns.map(e => [e.referencedColumn!.propertyName, true]) : rel.inverseRelation!.joinColumns.map(e => [e.databaseName, true])),
      where
    }, this.connection, queryRunner)
  }

  #create(rel: RelationMetadata, row1: any, row2: any, connection: DataSource, queryRunner: QueryRunner) {
    const data = rel.inverseRelation!.joinColumns.reduce((o, e) => (o[e.databaseName] = e.referencedColumn!.getEntityValue(row1), o), {} as any)
    return Create(rel.inverseEntityMetadata, { data: { ...row2, ...data } }, connection, queryRunner)
  }

  async #connect(rel: RelationMetadata, row1: any, where: any, connection: DataSource, queryRunner: QueryRunner) {
    const data = rel.inverseRelation!.joinColumns.reduce((o, e) => (o[e.databaseName] = e.referencedColumn!.getEntityValue(row1), o), {} as any)
    return await UpdateMany(rel.inverseEntityMetadata, { data, where }, connection, queryRunner)
  }

  async #nullFK(rel: RelationMetadata, reldata: any, connection: DataSource, queryRunner: QueryRunner) {
    if (rel.isOneToOneOwner) {
      const nulls = rel.joinColumns.reduce((o, e) => (o[e.databaseName] = null, o), {} as any)
      const where = rel.joinColumns.reduce((o, e) => (o[e.databaseName] = e.referencedColumn!.getEntityValue(reldata), o), {} as any)
      await UpdateMany(rel.entityMetadata, { data: nulls, where }, connection, queryRunner)
    }
  }
}


export const Create = (...args:  ConstructorParameters<typeof _Create>) => {
  return new _Create(...args).execute()
}